fix(mac): notice a deleted database, retire the bootstrap password, refuse concurrent restarts - #234
Merged
Merged
Conversation
…efuse concurrent restarts Three defects found by walking the install path by hand. **A deleted database went unnoticed.** needsFirstRun looked only for ~/.cix/server.env. Delete ~/.cix/data and the app carried on as a configured install: the server recreated the directory itself, found it empty, and minted a fresh admin from the bootstrap credentials still sitting in server.env — using the original generated password rather than whichever one the user had set since. Observed live at 18:31, "bootstrap admin created from CIX_BOOTSTRAP_ADMIN_EMAIL + CIX_BOOTSTRAP_ADMIN_PASSWORD" in the server log, with nothing on screen to say a database had gone. Setup is now needed when either the config file or the database it names is missing. **The bootstrap password never left.** It seeds the first admin and has no use afterwards, but it stayed on disk and stayed authoritative — a credential outliving the account it created, and quietly overriding a password rotated in the dashboard the next time the database was recreated. It is now removed once a running server proves the account exists (bootstrap runs before the listener opens, so an answering /health is that proof). The email stays: it is not a credential, and the reset-password dialog offers it as a default. Because the wizard can now run a second time, it merges onto the existing server.env instead of rebuilding it from defaults. Reverting someone's port or, worse, their network-access choice would be a bigger surprise than the one this removes. **Two menu items could restart the server at once.** Start at Login and Allow Network Access each restart it, each in its own goroutine, and each drives the same launchd label: one boots the job out while the other waits for its pid to disappear and then bootstraps it itself. Two bootstraps of one label leave either a failure or two processes racing for the port — from the outside, a server that went away and did not come back. Reported from use: click one, then the other during the restart, and the server hangs. A busy flag now gates every action that restarts the server, and render() respects it — without that the poller would re-enable the controls five seconds into the operation. The same guard covers a server that is still starting: a cold start loads an embedding model and takes minutes, and a restart landing in the middle of that is exactly the wedge. Serialising instead of refusing would only queue a second restart nobody asked for. Reset Password stays available throughout: it opens the database directly and neither needs a running server nor restarts one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three defects, all found by walking the install path by hand rather than by
testing it.
A deleted database went unnoticed
needsFirstRunlooked only for~/.cix/server.env. Delete~/.cix/dataandthe app carried on as a configured install — the server recreates the directory
itself (
db.godoesMkdirAll), finds it empty, and mints a fresh admin fromthe bootstrap credentials still sitting in
server.env.Observed live:
…with nothing on screen to say a database had gone, and with the original
generated password rather than whichever one had been set since.
Setup is now needed when either the config file or the database it names is
missing.
Not covered, and commented as such: a database file that exists but holds no
users. Answering that means opening SQLite from the launcher, which is a large
dependency for a case far rarer than "I deleted my data directory".
The bootstrap password never left
It seeds the first admin and has no purpose afterwards, but it stayed in
server.envand stayed authoritative — a credential outliving the account itcreated, and silently overriding a password rotated in the dashboard the next
time the database was recreated.
It is now dropped once a running server proves the account exists. Bootstrap
runs before the HTTP listener opens, so an answering
/healthis that proof.The email stays — it is not a credential, and the reset dialog offers it as the
address to reset.
Because the wizard can now run a second time, it merges onto the existing
server.envrather than rebuilding it from defaults. Reverting someone's port,or worse their network-access choice, would be a bigger surprise than the one
this removes.
Two menu items could restart the server at once
Reported from use: click Start at Login, then Allow Network Access
while the restart is running, and the server hangs.
Both restart the server, each in its own goroutine, and both drive the same
launchd label — one boots the job out while the other is waiting for its pid to
disappear and then bootstraps it itself. Two bootstraps of one label leave
either a failure or two processes racing for the port.
A busy flag now gates every action that restarts the server, and
render()respects it — without that the poller would re-enable the controls five seconds
into the operation. The same guard covers a server that is still starting,
since a cold start takes minutes and a restart landing mid-boot is the same
wedge. Serialising rather than refusing would only queue a second restart
nobody asked for.
Reset Password stays available throughout: it opens the database directly,
so it neither needs a running server nor restarts one.
Tests
needsFirstRunacross all four states,retireBootstrapPassword(removes onlythe password, keeps the API key and the bind address, holds mode 0600, is
idempotent), and that re-running setup keeps choices already made.
🤖 Generated with Claude Code